# encoding: utf-8

import math
useQtConsole=False # does not work yet
wooQApp=None

import wooMain
import sys, os

import woo
import woo.utils
log=woo.utils.makeLog('woo.qt')

if 'qt4' in woo.config.features: raise RuntimeError('Qt4 GUI is no longer supported.') 

if wooMain.options.fakeDisplay:
    # do nothing, let all the imports happen without error
    # running anything Qt-related will fail/crash later, this should be used only when generating docs
    pass
else:
    # signal import error without display
    if not woo.runtime.hasDisplay: raise ImportError(woo.runtime.hasDisplayError)

    # disable all IPython warnings (sometimes imported for the first time here, not in woo's __init__.py or wooMain)
    import warnings
    warnings.filterwarnings('ignore',category=DeprecationWarning,module='.*IPython.*')

    if 'qt5' in woo.config.features:
        from PyQt5 import QtGui
        from PyQt5.QtWidgets import QApplication

    if useQtConsole:
        from IPython.frontend.qt.console.qtconsoleapp import IPythonQtConsoleApp
        wooQApp=IPythonQtConsoleApp()
        wooQApp.initialize()
    else:
        # create an instance of InteractiveShell before the inputhook is created
        # see http://stackoverflow.com/questions/9872517/how-to-embed-ipython-0-12-so-that-it-inherits-namespace-of-the-caller for details
        # fixes http://gpu.doxos.eu/trac/ticket/40
        from IPython.terminal.embed import InteractiveShellEmbed # IPython>=1.0
        try: from traitlets.config.configurable import MultipleInstanceError
        except: from IPython.config.configurable import MultipleInstanceError # produces ShimWarning
        try: ipshell=InteractiveShellEmbed.instance()
        except MultipleInstanceError:
            log.warning('Already running inside ipython, not embedding new instance.')
        # keep the qapp object referenced, otherwise 0.11 will die with "QWidget: Must construct QApplication before a QPaintDevice
        # see also http://ipython.org/ipython-doc/dev/interactive/qtconsole.html#qt-and-the-qtconsole

        import IPython
        # see https://github.com/ipython/ipython/issues/9784 for discussion
        # the resolution not being very clear yet
        import woo.runtime
        if woo.runtime.ipython_version()>=500:
            wooQApp=QApplication(['woo.qt'])
            # the rest is done in wooMain, once the ipshell object is instantiated
            # -- ipshell.enable_gui('qt5')
        else:
            # this is deprecated in IPython >= 5.x
            import IPython.lib.inputhook #guisupport
            wooQApp=IPython.lib.inputhook.enable_gui(gui='qt4' if 'qt4' in woo.config.features else 'qt5')
    # try: wooQApp.setStyleSheet(open(woo.config.resourceDir+'/qmc2-black-0.10.qss').read())
    # except IOError: pass # stylesheet not readable or whatever
    if sys.platform=='win32': 
        # don't use ugly windows theme, try something else
        for style in QtGui.QStyleFactory.keys():
            # the first one will be used
            if  style in ('Cleanlooks','Plastique'):
                QtGui.QApplication.setStyle(QtGui.QStyleFactory.create(style))
                QtGui.QApplication.setPalette(QtGui.QApplication.style().standardPalette())
                break

import woo.config
if 'qt5' in woo.config.features:
    from PyQt5.QtGui import *
    from PyQt5.QtWidgets import *
    from PyQt5 import QtCore


from woo.qt.ui_controller import Ui_Controller

from woo.qt.Inspector import *
import woo,woo.system, woo.config

try:
    from woo._qt import *
    import woo._GLViewer
    from woo._GLViewer import *
    OpenGL=True
    # document those with woo.qt
    import woo._GLViewer
    _docInlineModules=(woo._qt,woo._GLViewer)
    # load preferences, if the file exists
except ImportError:
    OpenGL=False
    if 'opengl' in woo.config.features: raise RuntimeError("Woo was compiled with the 'opengl' feature, but OpenGL modules (woo._qt, woo._qt._GLViewer) could not be imported?")


class UiPrefs(woo.core.Object,woo.pyderived.PyWooObject):
    'Storage for local user interface preferences. This class is always instantiated as ``woo.qt.uiPrefs``; if the file :obj:`prefsFile` exists at startup, it is loaded automatically.'
    _classTraits=None
    _PAT=woo.pyderived.PyAttrTrait
    _attrTraits=[
        _PAT(bool,'glRotCursorFreeze',False,triggerPostLoad=True,doc='Freeze cursor when rotating things (moving with buttons pressed); useful for trackballs; not yet functional.'),
        _PAT(bool,'prepShowVars',False,'Show variable names rather than descriptions by default in the preprocessor interface'),
        _PAT(bool,'paraviewLike3d',True,triggerPostLoad=True,doc='Paraview-like mouse bindings for the 3d view (requires restart): middle-drag to move, left-drag to zoom. IF false, use old (Yade) behavior, which is the opposite.'),
        _PAT(str,'defaultCmap','coolwarm',choice=woo.master.cmaps,colormap=True,triggerPostLoad=True,doc='Default colormap for scales.'),
        # where to save preferences, plus a button to do so
        _PAT(str,'prefsFile',woo.master.confDir+'/uiPrefs.conf',guiReadonly=True,noDump=True,buttons=(['Save preferences','self.savePrefs()',''],0),doc='Preferences will be loaded/saved from/to this file.'),
    ]
    def postLoad(self,I):
        try:
            # sync with the flag inside glviewer
            import _GLViewer
            _GLViewer.GLViewer.rotCursorFreeze=self.glRotCursorFreeze
            _GLViewer.GLViewer.paraviewLike3d=self.paraviewLike3d
        except ImportError: pass # no OpenGL
        woo.master.cmap=self.defaultCmap
    def __new__(klass,**kw):
        self=super().__new__(klass)
        self.wooPyInit(klass,woo.core.Object,**kw)
        return self
    def __init__(self,**kw):
        woo.core.Object.__init__(self)
        self.wooPyInit(self.__class__,woo.core.Object,**kw)
    def savePrefs(self):
        import os, os.path, logging
        d=os.path.dirname(self.prefsFile)
        if not os.path.exists(d): os.makedirs(d)
        self.dump(self.prefsFile,format='expr')

uiPrefs=UiPrefs()
# if config file exists, uiPrefs are loaded in Controller.__init__

from .ExceptionDialog import *

#maxWebWindows=1
#"Number of webkit windows that will be cycled to show help on clickable objects"
#"holds instances of QtWebKit windows; clicking an url will open it in the window that was the least recently updated"
#webWindows=[] 
#webController=None

def onSelection(obj):
    print('woo.qt.onSelection:',obj,'was selected')
    print('   Set woo.gl.Renderer.selFunc to your own callback function.')

def openUrl(url):
    import webbrowser
    webbrowser.open(url)
    return

controller=None

class ControllerClass(QWidget,Ui_Controller):
    #from woo.gl import *
    #from woo import gl
    def __init__(self,parent=None):
        QWidget.__init__(self)
        self.setupUi(self)
        self.generator=None # updated automatically
        if OpenGL:
            # self.renderer=Renderer() # only hold one instance, managed by OpenGLManager
            self.addRenderers()

        # if config file exists already, load the config from that file instead
        import os.path
        global uiPrefs
        if os.path.exists(uiPrefs.prefsFile): uiPrefs=UiPrefs.load(uiPrefs.prefsFile)

        self.preIndexOther=-1
        self.genChecks,self.genVars=False,uiPrefs.prepShowVars
        self.refreshPreprocessors()
        self.fillAboutData()
        self.generatorComboSlot(None)
        self.movieButton.setEnabled(False)
        self.lastScene=None
        self.lastGl=None
        self.traceCheckboxToggled(isOn=None) # detect on/off
        self.movieActive=False
        self.movieFileEdit.setText(woo.utils.fixWindowsPath(str(self.movieFileEdit.text()))) # fix /tmp on windows
        self.tracerActive=False
        self.inspector=None

        # preprocessor submenu
        preSubMenu=QMenu()
        preSubCurr=preSubMenu.addAction(u'▶ Current simulation')
        preSubCurr.triggered.connect(lambda: self.generatorComboSlot(genStr=None,menuAction='loadCurrent'))
        preSubLoad=preSubMenu.addAction(u'↥ Load')
        preSubLoad.triggered.connect(lambda: self.generatorComboSlot(genStr=None,menuAction='loadFile'))
        preSubLib=makeLibraryBrowser(preSubMenu,lambda name,obj: self.generatorComboSlot(genStr=None,menuAction='loadLibrary',menuData=(name,obj)),woo.core.Preprocessor,u'⇈ Library')
        preSubMenu.addSeparator()
        r=preSubMenu.addAction(u'↻ Refresh preprocessors')
        r.triggered.connect(lambda: self.refreshPreprocessors())
        self.preMenuButton.setMenu(preSubMenu)
    
        if OpenGL:
            # display submenu
            dispMenu=QMenu()
            from . import DisplayProfiles
            for label,func in DisplayProfiles.predefinedProfiles.items():
                a=dispMenu.addAction(label)
                a.triggered.connect(func)
            if len(DisplayProfiles.predefinedProfiles)==0: self.dispMenuButton.setEnabled(False)
            else: self.dispMenuButton.setMenu(dispMenu)
        else: self.dispMenuButton.setEnabled(False)

        global controller
        controller=self
        self.setWindowTitle('Woo ('+woo.config.prettyVersion(lead=True)+(', debug' if woo.config.debug else '')+')')
        self.refreshTimer=QtCore.QTimer()
        self.refreshTimer.timeout.connect(self.refreshEvent)
        self.refreshTimer.start(200)
        self.stepLabel=self.iterLabel # name change
        self.stepPerSecTimeout=1 # how often to recompute the number of steps per second
        self.stepTimes,self.stepValues,self.stepPerSec=[],[],0 # arrays to keep track of the simulation speed
        #
        if not OpenGL:
            self.show3dButton.setText("[no OpenGL]")
            self.show3dButton.setEnabled(False)
        # show off with this one as well now
    def refreshPreprocessors(self):
        self.generatorCombo.clear()
        self.preprocessorObjects=[]
        # import all preprocessors
        import pkgutil, woo.pre, sys
        for importer,modname,ispkg in pkgutil.iter_modules(woo.pre.__path__):
            try: __import__('woo.pre.'+modname,fromlist='woo.pre')
            except:
                # this error is spurious with frozen installs, but probably meaningful otherwise
                # so we silently skip over when frozen
                if not hasattr(sys,'frozen'): print("(Error importing woo.pre."+modname+", ignoring.)")    
        #
        preps=[]
        for c in woo.system.childClasses(woo.core.Preprocessor):
            preps.append((c.__module__,c.__name__,c))
        preps.sort()
        for pre in preps:
            self.generatorCombo.addItem(pre[1]+('' if pre[0]=='woo.pre' else ' ('+pre[0]+')'))
            self.preprocessorObjects.append(pre[2])
        self.generatorCombo.insertSeparator(self.generatorCombo.count())
        self.generatorCombo.addItem(u'−')
        self.preIndexOther=self.generatorCombo.count()-1
        # make this item non-selectable by the user
        self.generatorCombo.model().setData(self.generatorCombo.model().index(self.preIndexOther,0),0,QtCore.Qt.UserRole-1)
        # refresh the view
        self.generatorComboSlot(0)
    def addRenderers(self):
        from woo import gl
        S=woo.master.scene
        rr=S.renderer # this will create S.gl if not there yet
        for i,f in enumerate(S.gl.objs):
            if f is None: self.displayCombo.insertSeparator(1000)
            else:
                if len(set(f.dict().keys())-set(['label']))>0: self.displayCombo.addItem(S.gl.objNames[i])

        self.displayCombo.insertSeparator(10000)
        self.displayCombo.addItem('UI Preferences')
    def displayComboSlot(self,dispStr):
        from woo import gl
        dispStr=str(dispStr) # cast from QString if still used (??)
        if dispStr=='UI Preferences':
            ser,path=uiPrefs,'woo.qt.uiPrefs'
        else:
            S=woo.master.scene
            ser,path=getattr(S.gl,dispStr),'S.gl.'+dispStr
        se=ObjectEditor(ser,parent=self.displayArea,ignoredAttrs=set(['label']),showType=True,path=path)
        self.displayArea.setWidget(se)
    def fillAboutData(self):    
        import woo, woo.config, platform, textwrap, types, pkg_resources, collections
        extras=[]
        try:
            import wooExtra
            ExInfo=collections.namedtuple('ExInfo','name, mod, version, distributor')
            for exName in dir(wooExtra):
                mod=getattr(wooExtra,exName)
                if type(mod)!=types.ModuleType: continue
                modName=mod.__name__
                ver=pkg_resources.get_distribution(modName).version
                distributor=str(getattr(mod,'distributor') if hasattr(mod,'distributor') else u'−')
                extras.append(ExInfo(name=exName,mod=mod,version=ver,distributor=distributor))
        except ImportError: pass # no wooExtra modules installed
        user=woo.master.scene.tags['user']
        self.aboutGeneralLabel.setText('''<h4>System data</h4><table cellpadding='2px' rules='all' width='100%'>
            <tr><td>user</td><td>{user}</td></tr>
            <tr><td>cores</td><td>{nCores}</td></tr>
            <tr><td>version</td><td>{version} ({buildDate}{flavor})</td></tr>
            <tr><td>platform</td><td>{platform}</td></tr>
            <tr><td>features&nbsp;</td><td>{features}</td></tr>
            <tr><td>extras</td><td>{extraModules}</td></tr>
        </table>
        '''.format(user=user,nCores=woo.master.numThreads,platform='<br>'.join(textwrap.wrap(platform.platform().replace('-',' '),40)),version=woo.config.version+('' if woo.config.version==woo.config.revision else ('/' if woo.config.version else '')+woo.config.revision)+(' (debug)' if woo.config.debug else ''),features=', '.join(woo.config.features),buildDate=woo.config.buildDate,flavor=((', flavor "'+woo.config.flavor+'"') if woo.config.flavor else ''),extraModules='<br>'.join(['{e.name} ({e.version})'.format(e=e) for e in extras])))
        if extras:
            self.aboutExtraLabel.setText(u"<h4>Extra modules</h4><table cellpadding='10px' width='100%'>"+u''.join([u'<tr><td>wooExtra.<b>{e.name}&nbsp;</b><br>{e.version}</td><td>{e.distributor}</td></tr>'.format(e=e) for e in extras])+'</table')
    def inspectSlot(self):
        if not self.inspector:
            self.inspector=SimulationInspector(parent=None)
            self.inspector.show()
        else:
            #self.inspector.hide()
            self.inspector=None
    def setTabActive(self,what):
        if what=='simulation': ix=0
        elif what=='display': ix=1
        elif what=='generator': ix=2
        elif what=='python': ix=3
        else: raise ValueErorr("No such tab: "+what)
        self.controllerTabs.setCurrentIndex(ix)
    def generatorComboSlot(self,genStr,menuAction=None,menuData=None):
        "update generator parameters when a new one is selected"
        ix=self.generatorCombo.currentIndex()
        if menuAction==None:
            self.generator=(self.preprocessorObjects[ix]() if ix<len(self.preprocessorObjects) else None)
            self.generatorCombo.setItemText(self.preIndexOther,u'−')
        elif menuAction=='loadFile':
            f=QFileDialog.getOpenFileName(self,'Load preprocessor from','.')
            if isinstance(f,tuple): f=f[0]
            f=str(f)
            self.generatorCombo.setCurrentIndex(self.preIndexOther)
            if not f:
                self.generator=None
                self.generatorCombo.setItemText(self.preIndexOther,u'−')
                self.generatorArea.setWidget(QLabel('<i>No preprocessor loaded</i>'))
            else:
                try:
                    self.generator=Preprocessor.load(f)
                    self.generatorCombo.setItemText(self.preIndexOther,'%s'%f)
                except Exception as e:
                    import traceback
                    traceback.print_exc()
                    showExceptionDialog(self,e)
        elif menuAction=='loadLibrary':
            name,obj=menuData
            self.generatorCombo.setCurrentIndex(self.preIndexOther)
            self.generatorCombo.setItemText(self.preIndexOther,'/'.join(name))
            self.generator=obj.deepcopy()
        elif menuAction=='loadCurrent':
            self.generatorCombo.setCurrentIndex(self.preIndexOther)
            if not woo.master.scene.pre:
                self.generatorArea.setWidget(QLabel('<i>No preprocessor in <b>Scene.pre</t></i>'))
                self.generator=None
                self.generatorCombo.setItemText(self.preIndexOther,u'−')
            else:
                # force deep-copy, to avoid bug #88
                self.generator=woo.master.scene.pre.deepcopy()
                self.generatorCombo.setItemText(self.preIndexOther,u'current simulation')
        if self.generator:
            # keep var/check settings
            w=self.generatorArea.widget()
            if w and isinstance(w,ObjectEditor): self.genVars,self.genChecks=w.labelIsVar,w.showChecks
            se=ObjectEditor(self.generator,parent=self.generatorArea,showType=True,labelIsVar=self.genVars,showChecks=self.genChecks,showUnits=True,objManip=True) # TODO
            self.generatorArea.setWidget(se)
    def genSaveParamsSlot(self):
        if 0:
            formats=[('expr','Text, loadable (*.expr)'),('pickle','Pickle, loadable (*.pickle)'),('xml','XML (loadable)'),('HTML (write-only)','html')]
            dialog=QFileDialog(None,'Save preprocessor parameters to','.',';; '.join([fmt[1] for fmt in formats]))
            dialog.setAcceptMode(QFileDialog.AcceptSave)
            f=dialog.exec_()
            print('chosen file',str(dialog.selectedFiles()[0]))
            if not f: return # cancelled
        else:
            f=QFileDialog.getSaveFileName(self,'Save preprocessor parameters','.')
            if isinstance(f,tuple): f=f[0]
            f=str(f)
            if not f: return
            self.generator.dump(f,format='json' if f.endswith('.json') else 'expr')
    def generateSlot(self):
        try:
            QApplication.setOverrideCursor(QtCore.Qt.BusyCursor)
            out=str(self.generatorFilenameEdit.text())
            mem=self.genMemoryCombo.currentIndex()==0
            auto=self.genOpenCheckbox.isChecked()
            pre=self.generator
            newScene=pre()
            if not isinstance(newScene,woo.core.Scene): raise RuntimeError('Preprocessor must return a Scene when called, not '+str(newScene))
            if mem: newScene.saveTmp(out)
            elif out: newScene.save(out)
            else: pass # only generate, don't save
            if auto:
                if woo.master.scene.running:
                    import sys
                    sys.stdout.write('Stopping the current simulation...')
                    woo.master.scene.stop()
                    print(' ok')
                woo.master.scene=newScene
                controller.setTabActive('simulation')
        except Exception as e:
            import traceback
            traceback.print_exc()
            showExceptionDialog(self,e)
        finally:
            QApplication.restoreOverrideCursor()

    def throttleLin2Exp(self,x):
        r'Convert integer value of the throttle control to Scene.throttle (in seconds). We map the range (0..xmax) to (0...ymax) with :math:`y(x)=\frac{\log x+1}{\log x_{\max}+1}y_{\max}`.'
        xmax=self.throttleControl.maximum()
        ymax=0.5 # sync with the same constant in throttleLin2Exp
        beta=3.
        return ymax*(x*1./xmax)**beta
    def throttleExp2Lin(self,y):
        r'Convert Scene.throttle (in seconds) to the position of the throttle control. This is the inverse of :obj:`throttleInt2Float` giving :math:`x(y)=\exp\left(\frac{y}{y_{\max}}\log(x_{\max}+1)\right)-1`.'
        xmax=self.throttleControl.maximum()
        ymax=0.5 # synced with the same constant in throttleExp2Lin
        beta=3.
        return xmax*(y/ymax)**(1./beta)
    def throttleChanged(self,value):
        if value>0: self.playButton.setStyleSheet('background-color: red; ')
        else: self.playButton.setStyleSheet('')
        woo.master.scene.throttle=self.throttleLin2Exp(value)
    def movieCheckboxToggled(self,isOn,findExisting=False):
        S=woo.master.scene
        snap=None
        if findExisting: # done when the scene changes
            ss=[e for e in S.engines if isinstance(e,woo.qt.SnapshotEngine)]
            if ss:
                if len(ss)>1: warnings.warn('Multiple SnapshotEngines?! (proceed with fingers crossed; if one of them disappears, it is the UI Video tab trying to enable/disable it by itself)')
                snap=ss[0]
                if snap.label and snap.label!='snapshooter_':
                    warnings.warn('Setting SnapshotEngine.label="snapshooter_" automatically, for integration with the UI')
                    del S.labels[snap.label]
                snap.label='snapshooter_'
                S.engines=S.engines
        if isOn or snap:
            if hasattr(S.lab,'snapshooter_'): snap=S.lab.snapshooter_
            # add SnapshotEngine to the current scene
            if not snap:
                snap=woo.qt.SnapshotEngine(label='snapshooter_',fileBase=woo.master.tmpFilename(),stepPeriod=100,realPeriod=.5,ignoreErrors=False)
                S.engines=S.engines+[snap]
            se=ObjectEditor(snap,parent=self.movieArea,showType=True)
            self.movieArea.setWidget(se)
            # open new view if there is none            
            if len(views())==0: View()
            self.movieButton.setEnabled(True)
            self.movieActive=True
            if not self.movieCheckbox.isChecked(): self.movieCheckbox.setChecked(True) # force the checkbox
        else:
            if hasattr(S.lab,'snapshooter_'): del S.lab.snapshooter_
            woo.master.scene.engines=[e for e in S.engines if type(e)!=woo.qt.SnapshotEngine]
            self.movieArea.setWidget(QFrame())
            self.movieButton.setEnabled(False)
            self.movieActive=False
    def movieButtonClicked(self):
        S=woo.master.scene
        if not hasattr(S.lab,'snapshooter_'):
            print('No S.lab.snapshooter_, no movie will be created')
            return
        out=str(self.movieFileEdit.text())
        woo.utils.makeVideo(S.lab.snapshooter_.snapshots,out=out,fps=self.movieFpsSpinbox.value(),kbps=self.movieBitrateSpinbox.value())
        import os.path
        url='file://'+os.path.abspath(out)
        print('Video saved to',url)
        import webbrowser
        webbrowser.open(url)
    def resetTraceClicked(self):
        S=woo.master.scene
        if not hasattr(S.lab,'tracer_'):
            print('No S.lab.tracer_, will not reset')
            return
        S.lab.tracer_.resetNodesRep(setupEmpty=True)
    def tracerGetEngine(self,S):
        'Find tracer engine, return it, or return None; raise exception with multiple tracers'
        tt=[e for e in S.engines if isinstance(e,woo.dem.Tracer)]
        if len(tt)>1: raise RuntimeError('Multiple Tracer engines in scene?')
        if len(tt)==1: return tt[0]
        return None
    def traceCheckboxToggled(self,isOn):
        S=woo.master.scene
        # auto-detect on/off
        if isOn==None:
            tr=self.tracerGetEngine(woo.master.scene)
            isOn=bool(tr and not tr.dead)
            # will call this function again, and also set the checkbox state properly
            self.traceCheckbox.setChecked(isOn) 
            return 
        # go ahead now
        if isOn:
            # is there a tracer already?
            tracer=self.tracerGetEngine(S)
            if not tracer:
                tracer=woo.dem.Tracer(label='tracer_',stepPeriod=100,realPeriod=.2)
                S.engines=S.engines+[tracer]
            else:
                tracer.label='tracer_' # hijack existing engine and re-label it
                S.engines=S.engines # force re-creation of labels
            tracer.dead=False
            S.ranges=S.ranges+[tracer.lineColor]
            self.tracerArea.setWidget(ObjectEditor(tracer,parent=self.tracerArea,showType=True))
            self.resetTraceButton.setEnabled(True)
            woo.gl.Gl1_DemField.glyph=woo.gl.Gl1_DemField.glyphKeep
            self.tracerActive=True
        else:
            tracer=self.tracerGetEngine(S)
            if tracer:
                tracer.dead=True
                #hasattr(woo,'_tracer'):
                tracer.resetNodesRep(setupEmpty=False)
                #del woo._tracer
            #S.engines=[e for e in S.engines if type(e)!=woo.dem.Tracer]
            S.ranges=[r for r in S.ranges if r!=tracer.lineColor]
            self.tracerArea.setWidget(QFrame())
            self.resetTraceButton.setEnabled(False)
            self.tracerActive=False
        
    def loadSlot(self):
        f=QFileDialog.getOpenFileName(self,'Load simulation','')
        if isinstance(f,tuple): f=f[0]
        f=str(f)
        if not f: return # cancelled
        self.deactivateControls()
        woo.master.scene=Scene.load(f)
    def saveSlot(self):
        f=QFileDialog.getSaveFileName(self,'Save simulation','')
        if isinstance(f,tuple): f=f[0]
        f=str(f)
        if not f: return # cancelled
        woo.master.scene.save(f)
        import os.path
        assert os.path.exists(f),"File '%s' does not exist after saving?"%f
    def reloadSlot(self):
        self.deactivateControls()
        p=woo.master.scene.plot
        p.splitData()
        woo.master.scene.stop()
        woo.master.reload()
        # assign including previous data
        # this should perhaps not be the default?
        woo.master.scene.plot=p 
    def playSlot(self):    woo.master.scene.run()
    def pauseSlot(self): woo.master.scene.stop()
    def stepSlot(self):
        S=woo.master.scene
        if self.multiStepSpinBox.value()==1 or S.subStepping: S.one()
        else:
            S.run() # don't block with multistep
            S.stopAtStep=S.step+self.multiStepSpinBox.value()
    def subStepSlot(self,value):
        self.multiStepSpinBox.setEnabled(not bool(value))
        woo.master.scene.subStepping=bool(value)
    def show3dSlot(self, show):
        vv=views()
        assert(len(vv) in (0,1))
        if show:
            if len(vv)==0: View()
        else:
            if len(vv)>0: vv[0].close()
    def plotSlot(self):
        woo.master.scene.plot.plot()
    def refreshEvent(self):
        self.refreshValues()
        self.activateControls()
    def deactivateControls(self):
        self.realTimeLabel.setText('')
        self.virtTimeLabel.setText('')
        self.stepLabel.setText('')
        self.fileLabel.setText('<i>[loading]</i>')
        self.playButton.setEnabled(False)
        self.pauseButton.setEnabled(False)
        self.stepButton.setEnabled(False)
        self.subStepCheckbox.setEnabled(False)
        self.reloadButton.setEnabled(False)
    def activateControls(self):
        S=woo.master.scene
        hasSim=len(S.engines)>0
        running=S.running
        if hasSim:
            self.playButton.setEnabled(not running)
            self.pauseButton.setEnabled(running)
            self.reloadButton.setEnabled(bool(S.lastSave))
            self.stepButton.setEnabled(not running)
            self.subStepCheckbox.setEnabled(not running)
        else:
            self.playButton.setEnabled(False)
            self.pauseButton.setEnabled(False)
            self.reloadButton.setEnabled(False)
            self.stepButton.setEnabled(False)
            self.subStepCheckbox.setEnabled(False)
        self.traceCheckbox.setEnabled(S.hasDem)
        fn=S.lastSave
        self.fileLabel.setText(fn if fn else '[no file]')

        #
        #
        self.plotButton.setEnabled(len(S.plot.plots)>0)

    def refreshValues(self):
        S=woo.master.scene
        rt=int(woo.master.realtime); t=S.time; step=S.step;
        assert(len(self.stepTimes)==len(self.stepValues))
        if len(self.stepTimes)==0: self.stepTimes.append(rt); self.stepValues.append(step); self.stepPerSec=0 # update always for the first time
        elif rt-self.stepTimes[-1]>self.stepPerSecTimeout: # update after a timeout
            if len(self.stepTimes)==1: self.stepTimes.append(self.stepTimes[0]); self.stepValues.append(self.stepValues[0]) # 2 values, first one is bogus
            self.stepTimes[0]=self.stepTimes[1]; self.stepValues[0]=self.stepValues[1]
            self.stepTimes[1]=rt; self.stepValues[1]=step;
            self.stepPerSec=(self.stepValues[-1]-self.stepValues[-2])*1./(self.stepTimes[-1]-self.stepTimes[-2])
        if not S.running: self.stepPerSec=0
        stopAtStep=S.stopAtStep
        subStepInfo=''
        if S.subStepping:
            subStep=S.subStep
            if subStep==-1: subStepInfo=u'→ <i>prologue</i>'
            elif subStep>=0 and subStep<len(S.engines):
                e=S.engines[subStep]; subStepInfo=u'→ %s'%(e.label if e.label else e.__class__.__name__)
            elif subStep==len(S.engines): subStepInfo=u'→ <i>epilogue</i>'
            else: raise RuntimeError("Invalid S.subStep value %d, should be ∈{-1,…,len(o.engines)}"%subStep)
            subStepInfo="<br><small>sub %d/%d [%s]</small>"%(subStep,len(S.engines),subStepInfo)
        self.subStepCheckbox.setChecked(S.subStepping) # might have been changed async
        throttle='' if S.throttle==0. else '<font color="red">(max %d/s)</font>'%(int(round(1./S.throttle)))
        eta=-1
        if stopAtStep<=step:
            self.realTimeLabel.setText('%02d:%02d:%02d'%(rt//3600,(rt%3600)//60,rt%60))
            self.stepLabel.setText('#%ld, %.1f/s %s%s'%(step,self.stepPerSec,throttle,subStepInfo))
        else:
            if self.stepPerSec!=0:
                eta=int((stopAtStep-step)/self.stepPerSec)
            else: eta=0 # u' (ETA −)'
            self.stepLabel.setText('#%ld / %ld, %.1f/s %s%s'%(S.step,stopAtStep,self.stepPerSec,throttle,subStepInfo))
        stopAtTime=(S.stopAtTime if (not math.isnan(S.stopAtTime) and S.stopAtTime>S.time) else 0)
        if stopAtTime: eta=(int((stopAtTime-S.time)/(S.time/rt)) if S.time>0 else 0)

        if eta<0: etaStr=''
        elif eta==0: etaStr=u' (ETA −)'
        else: etaStr=' (ETA %02d:%02d:%02d)'%(eta//3600,(eta%3600)//60,eta%60)

        self.realTimeLabel.setText('%02d:%02d:%02d%s'%(rt//3600,(rt%3600)//60,rt%60,etaStr))
        if t!=float('inf') and t!=float('nan'):
            def t2smun(t): return int(t),int(t*1000)%1000,int(t*1000000)%1000,int(t*1000000000)%1000
            def smun2strippedstr(smun):
                if sum(smun)==0: return '000s'
                ret=''
                for i in range(4):
                    if not ret and smun[i]==0: continue
                    if sum(smun[i:])==0: continue
                    ret+='%03d'%smun[i]+u'smμn'[i]
                return ret
            self.virtTimeLabel.setText(u'%03ds%03dm%03dμ%03dn'%t2smun(t)+(u' / '+smun2strippedstr(t2smun(S.stopAtTime)) if stopAtTime else ''))
            #s=int(t); ms=int(t*1000)%1000; us=int(t*1000000)%1000; ns=int(t*1000000000)%1000
        else: self.virtTimeLabel.setText(u'[ ∞ ] ?!')
        self.dtLabel.setText(str(S.dt))
        if OpenGL: self.show3dButton.setChecked(len(views())>0)
        self.inspectButton.setChecked(self.inspector!=None)
        #
        if not self.throttleControl.isSliderDown():
            v=self.throttleExp2Lin(S.throttle)
            if self.throttleControl.value!=v: self.throttleControl.setValue(int(v))
        ##
        if self.lastScene!=S:
            self.lastScene=S
            # set movie and trace using last state
            if OpenGL:
                self.movieCheckboxToggled(self.movieActive,findExisting=True)
                self.traceCheckboxToggled(isOn=None)
            #
            #
            ix=self.simPageLayout.indexOf(self.customArea)
            row,col,rows,cols=self.simPageLayout.getItemPosition(ix) # find position, so that we know where to place the next one
            self.simPageLayout.removeWidget(self.customArea)
            self.customArea.setParent(None)
            self.customArea=QScrollArea()
            sp=QSizePolicy(QSizePolicy.Expanding,QSizePolicy.Expanding); sp.setVerticalStretch(40)
            self.customArea.setSizePolicy(sp)
            self.simPageLayout.addWidget(self.customArea,row,col)
            #print 'New QScrollArea created'
            if S.uiBuild:
                import __main__
                glob=globals(); glob.update(__main__.__dict__)
                exec(S.uiBuild, glob, {'S':S,'area':self.customArea})
        ## renderer change
        if self.lastGl!=S.gl:
            self.displayComboSlot(self.displayCombo.currentText())
            self.lastGl=S.gl
            

        
def Generator():
    global controller
    if not controller: controller=ControllerClass();
    controller.show(); controller.raise_()
    controller.setTabActive('generator')
def Controller():
    global controller
    if not controller: controller=ControllerClass();
    controller.show(); controller.raise_()
    controller.setTabActive('simulation')
def Inspector():
    global controller
    if not controller: controller=ControllerClass();
    controller.inspectSlot()
